package pl.com.qapps.datastore;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.Future;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

/**
 * The stack does not share transactions between threads and between two
 * different instances in the same thread, in other words it share transaction
 * in the same thread using this stack.
 * 
 * @author Jakub Dykowski
 * 
 */
public class DefaultTransactionStack implements TransactionStack {

	private ThreadLocal<Stack> stack = new ThreadLocal<Stack>();

	@Override
	public void push(Transaction txn) {
		getStack().txns.push(txn);
	}

	@Override
	public void remove(Transaction txn) {
		Stack stack = getStack();
		// TODO ensure that this is necessary or consider making it a set, not
		// dequeue
		while (stack.txns.remove(txn))
			;
		stack.data.remove(txn.getId());
	}

	@Override
	public Transaction peek() {
		Transaction current = getStack().txns.peek();
		if (current == null)
			throw new NoSuchElementException();
		return current;
	}

	@Override
	public Transaction peek(Transaction returnedIfNoTxn) {
		Transaction current = getStack().txns.peek();
		if (current == null)
			return returnedIfNoTxn;
		return current;
	}

	@Override
	public Collection<Transaction> getAll() {
		return getStack().txns;
	}

	@Override
	public void clearAll() {
		Stack stack = getStack();
		stack.txns.clear();
		stack.data.clear();
	}

	@Override
	public void addFuture(Transaction txn, Future<?> future) {
		getTxnData(txn).futures.add(future);
	}

	@Override
	public Set<Future<?>> getFutures(Transaction txn) {
		return getTxnData(txn).futures;
	}

	@Override
	public void addPutEntities(Transaction txn, List<Entity> putEntities) {
		getTxnData(txn).puts.addAll(putEntities);
	}

	@Override
	public List<Entity> getPutEntities(Transaction txn) {
		return getTxnData(txn).puts;
	}

	@Override
	public void addDeletedKeys(Transaction txn, List<Key> deletedKeys) {
		getTxnData(txn).deletes.addAll(deletedKeys);
	}

	@Override
	public List<Key> getDeletedKeys(Transaction txn) {
		return getTxnData(txn).deletes;
	}

	private TxnData getTxnData(Transaction txn) {
		TxnData txnData = getStack().data.get(txn.getId());
		if (txnData == null)
			getStack().data.put(txn.getId(), txnData = new TxnData());
		return txnData;
	}

	private Stack getStack() {
		Stack dataMap = stack.get();
		if (stack == null)
			stack.set(dataMap = new Stack());
		return dataMap;
	}

	private static class Stack {
		private final Deque<Transaction> txns = new LinkedList<Transaction>();
		private final Map<String, TxnData> data = new HashMap<String, TxnData>();
	}

	private static class TxnData {
		Set<Future<?>> futures = new LinkedHashSet<>();
		List<Key> deletes = new ArrayList<Key>();
		List<Entity> puts = new ArrayList<Entity>();
	}

}
